﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;
using System.IO;

namespace Watson.TemporaryFilesDeleter
{
    /// <summary>
    /// Provides a log of program activity, warnings and errors.
    /// </summary>
    public class Log : IDisposable
    {
        #region Private Static Fields

        /// <summary>
        /// The singleton instance of the Log class.
        /// </summary>
        private static readonly Log instance = new Log();

        #endregion

        #region Private Fields

        /// <summary>
        /// The current session settings which provide the basis for the 
        /// <see cref="BufferWritingLogToDisk"/>, <see cref="LogFilePath"/>,
        /// <see cref="LogLevel"/> and <see cref="WriteLogToDisk"/> properties.
        /// </summary>
        private readonly Properties.Settings currentSessionSettings = 
            Properties.Settings.CurrentSession;
        
        /// <summary>
        /// Tracks whether <see cref="Dispose"/> has been called.
        /// </summary>
        private bool disposed;
        
        /// <summary>
        /// A list of log entries.
        /// </summary>
        private List<LogItem> items = new List<LogItem>();

        /// <summary>
        /// Synchronisation object for accessing the <see cref="items"/>
        /// list.
        /// </summary>
        private readonly object itemsSyncRoot = new object();

        #endregion

        #region Public Events

        /// <summary>
        /// Occurs when a log item has been written to the log.
        /// </summary>
        public event EventHandler<LogItemWrittenEventArgs> LogItemWritten;

        #endregion
        
        #region Private Properties

        /// <summary>
        /// Gets a boolean indicating whether to buffer writing log items to
        /// disk.
        /// </summary>
        private bool BufferWritingLogToDisk
        {
            get { return this.currentSessionSettings.BufferLogFileWriting; }
        }
        
        /// <summary>
        /// Gets the full path to the log file.
        /// </summary>
        private string LogFilePath
        {
            get { return this.currentSessionSettings.LogFilePath; }
        }
        
        /// <summary>
        /// Gets the current log level. Only entries with a log level equal to
        /// or greater than the log level set here will be committed to the
        /// log.
        /// </summary>
        private LogLevel LogLevel
        {
            get { return this.currentSessionSettings.LogLevel; }
        }

        /// <summary>
        /// Gets a boolean indicating whether to write log items to disk.
        /// </summary>
        private bool WriteLogToDisk
        {
            get { return this.currentSessionSettings.WriteLogFile; }
        }

        #endregion

        #region Public Static Properties

        /// <summary>
        /// Gets the singleton instance of the Log class.
        /// </summary>
        public static Log Instance
        {
            get { return instance; }
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Adds a log item to the list of items in the log.
        /// </summary>
        /// <param name="item">
        /// The LogItem to add.
        /// </param>
        /// <exception cref="System.ArgumentNullException">
        /// <paramref name="item"/> was null.
        /// </exception>
        private void AddLogItem(LogItem item)
        {
            if (item == null)
                throw new ArgumentNullException("item");

            lock (this.itemsSyncRoot)
            {
                this.items.Add(item);

                if (this.WriteLogToDisk && !this.BufferWritingLogToDisk)
                {
                    this.WriteLogItemToDisk(item);
                }
            }

            OnLogItemWritten(item);
        }

        /// <summary>
        /// Flushes the write buffer and writes log items to disk (if writing
        /// to disk and write buffering are enabled).
        /// </summary>
        private void FlushWriteBuffer()
        {
            if (this.WriteLogToDisk && this.BufferWritingLogToDisk)
            {
                lock (this.itemsSyncRoot)
                {
                    this.WriteAllLogItemsToDisk();
                }
            }
        }

        /// <summary>
        /// Raises the <see cref="LogItemWritten"/> event.
        /// </summary>
        /// <param name="item">
        /// The log item was written to the log.
        /// </param>
        /// <exception cref="System.ArgumentNullException">
        /// <paramref name="item"/> was null.
        /// </exception>
        private void OnLogItemWritten(LogItem item)
        {
            if (item == null)
                throw new ArgumentNullException("item");

            EventHandler<LogItemWrittenEventArgs> temp = LogItemWritten;
            
            if (temp != null)
            {
                LogItemWrittenEventArgs eventArgs = 
                    new LogItemWrittenEventArgs(item);
                
                temp(this, eventArgs);
            }
        }

        /// <summary>
        /// Writes all log items to disk.
        /// </summary>
        private void WriteAllLogItemsToDisk()
        {
            foreach (var item in this.items)
            {
                this.WriteLogItemToDisk(item);
            }
        }

        /// <summary>
        /// Writes a log item to disk.
        /// </summary>
        /// <param name="item">
        /// The LogItem to write to disk.
        /// </param>
        /// <exception cref="System.ArgumentNullException">
        /// <paramref name="item"/> was null.
        /// </exception>
        private void WriteLogItemToDisk(LogItem item)
        {
            if (item == null)
                throw new ArgumentNullException("item");


            using (StreamWriter logFile = 
                new StreamWriter(this.LogFilePath, true))
            {
                // E.g. "2012-04-18 15:34:22 Error       Error test."
                string logLineFormat = "{0:yyyy-MM-dd HH:mm:ss} {1,-11} {2}";

                string logLine = String.Format(CultureInfo.CurrentCulture,
                                               logLineFormat,
                                               item.EnteredDateTime,
                                               item.LogLevel.ToString(),
                                               item.Text);

                logFile.WriteLine(logLine); 
            }
        }

        #endregion

        #region Protected Methods

        /// <summary>
        /// Disposes of unmanaged resources.
        /// </summary>
        ~Log()
        {
            this.Dispose(false);
        }

        /// <summary>
        /// Disposes managed and unmanaged resources.
        /// </summary>
        /// <param name="disposing">
        /// If true, managed and unmanaged resources will be disposed; else,
        /// only unmanaged resources will be disposed.
        /// </param>
        protected virtual void Dispose(bool disposing)
        {
            if (!this.disposed)
            {
                if (disposing)
                {
                    // Dispose managed resources here.
                    this.FlushWriteBuffer();
                }

                // Disposed unmanaged resources here.

                this.disposed = true;
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Disposes of managed resources.
        /// </summary>
        public void Dispose()
        {
            this.Dispose(true);
        }
        
        /// <summary>
        /// Writes an entry to the log.
        /// </summary>
        /// <param name="text">
        /// The text of the entry to write.
        /// </param>
        /// <exception cref="System.ArgumentNullException">
        /// <paramref name="text"/> was null.
        /// </exception>
        public void Write(string text)
        {
            if (text == null)
                throw new ArgumentNullException("text");
            
            this.Write(LogLevel.None, text);
        }

        /// <summary>
        /// Writes an error level entry to the log using information from a 
        /// specified exception.
        /// </summary>
        /// <param name="exception">
        /// The exception containing the details for the log entry.
        /// </param>
        /// <exception cref="System.ArgumentNullException">
        /// <paramref name="exception"/> was null.
        /// </exception>
        public void Write(Exception exception)
        {
            if (exception == null)
                throw new ArgumentNullException("exception");

            this.Write(LogLevel.Error, exception);
        }

        /// <summary>
        /// Writes an entry to the log with the specified log level.
        /// </summary>
        /// <param name="level">
        /// The level of the log entry to write.
        /// </param>
        /// <param name="text">
        /// The text of the entry to write.
        /// </param>
        /// <exception cref="System.ArgumentNullException">
        /// <paramref name="text"/> was null.
        /// </exception>
        public void Write(LogLevel level, string text)
        {
            if (level >= this.LogLevel)
            {
                LogItem logItem = new LogItem(level, text);

                this.AddLogItem(logItem);
            }
        }

        /// <summary>
        /// Writes an entry to the log with the specified log level using 
        /// information from a specified exception.
        /// </summary>
        /// <param name="level">
        /// The level of the log entry to write.
        /// </param>
        /// <param name="exception">
        /// The exception containing the details for the log entry.
        /// </param>
        /// <exception cref="System.ArgumentNullException">
        /// <paramref name="exception"/> was null.
        /// </exception>
        public void Write(LogLevel level, Exception exception)
        {
            if (exception == null)
                throw new ArgumentNullException("exception");

            // E.g. "System.ArgumentNullException: Argument cannot be null."
            string text = String.Format(CultureInfo.CurrentCulture,
                                        "{0}: {1}",
                                        exception.GetType().FullName,
                                        exception.Message);

            this.Write(level, text);
        }

        /// <summary>
        /// Writes a debug level entry to the log.
        /// </summary>
        /// <param name="text">
        /// The text of the entry to write.
        /// </param>
        /// <exception cref="System.ArgumentNullException">
        /// <paramref name="text"/> was null.
        /// </exception>
        public void WriteDebug(string text)
        {
            if (text == null)
                throw new ArgumentNullException("text");

            this.Write(LogLevel.Debug, text);
        }

        /// <summary>
        /// Writes an error level entry to the log.
        /// </summary>
        /// <param name="text">
        /// The text of the entry to write.
        /// </param>
        /// <exception cref="System.ArgumentNullException">
        /// <paramref name="text"/> was null.
        /// </exception>
        public void WriteError(string text)
        {
            if (text == null)
                throw new ArgumentNullException("text");

            this.Write(LogLevel.Error, text);
        }

        /// <summary>
        /// Writes an information level entry to the log.
        /// </summary>
        /// <param name="text">
        /// The text of the entry to write.
        /// </param>
        /// <exception cref="System.ArgumentNullException">
        /// <paramref name="text"/> was null.
        /// </exception>
        public void WriteInformation(string text)
        {
            if (text == null)
                throw new ArgumentNullException("text");

            this.Write(LogLevel.Information, text);
        }

        /// <summary>
        /// Writes a warning level entry to the log.
        /// </summary>
        /// <param name="text">
        /// The text of the entry to write.
        /// </param>
        /// <exception cref="System.ArgumentNullException">
        /// <paramref name="text"/> was null.
        /// </exception>
        public void WriteWarning(string text)
        {
            if (text == null)
                throw new ArgumentNullException("text");

            this.Write(LogLevel.Warning, text);
        }

        #endregion

        #region Private Constructors

        /// <summary>
        /// Initialises a new instance of the Log class.
        /// </summary>
        private Log()
        { }

        #endregion
    }
}
